package com.lpf;

import net.bytebuddy.ByteBuddy;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.dynamic.loading.ClassLoadingStrategy;
import net.bytebuddy.implementation.FieldAccessor;
import net.bytebuddy.implementation.FixedValue;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.implementation.SuperMethodCall;
import net.bytebuddy.implementation.bind.annotation.Morph;
import net.bytebuddy.matcher.ElementMatchers;
import org.junit.Test;

import java.lang.reflect.*;

import static net.bytebuddy.dynamic.loading.ClassLoadingStrategy.Default.INJECTION;
import static net.bytebuddy.dynamic.loading.ClassLoadingStrategy.Default.WRAPPER;
import static net.bytebuddy.matcher.ElementMatchers.named;

public class Main {

    /**
     * 创建一个新的类
     */
    @Test
    public void test_1() {
        // 生成新的类，但是没有加载
        DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
                .subclass(Object.class) // 生成 Object 的子类
                .name("com.lpf.Type")   // 生成类的名称为"com.lpf.Type"
                .make();

        // 生成新的类，并且加载
        Class<?> loaded = new ByteBuddy()
                .subclass(Object.class)
                .name("com.lpf.Type")
                .make()
                // 使用 WRAPPER 策略加载生成的动态类型
                .load(Main.class.getClassLoader(), WRAPPER)
                .getLoaded();
    }

    /**
     * 增强新创建的类
     *
     * 通过 method() 方法拦截到的所有方法会由 Intercept() 方法指定的 Implementation 对象决定如何增强。
     */
    @Test
    public void test_2() throws InstantiationException, IllegalAccessException {
        String str = new ByteBuddy() // 创建ByteBuddy对象
                .subclass(Object.class) // subclass增强方式
                .name("com.lpf.Type") // 新类型的类名
                .method(
                        ElementMatchers
                                .named("toString") // 指定方法名称
                                .and(ElementMatchers.returns(String.class)) // 指定方法的返回值
                                .and(ElementMatchers.takesArguments(0)) // 指定方法参数
                ) // 拦截其中的toString()方法
                .intercept(FixedValue.value("Hello World!")) // 让toString()方法返回固定值
                .make()
                .load(ByteBuddy.class.getClassLoader()) // 加载新类型，默认WRAPPER策略
                .getLoaded()
                .newInstance() // 通过Java反射创建com.lpf.Type实例
                .toString(); // 调用 toString()方法

        System.out.println(str);
    }

    /**
     * Byte Buddy 中可以设置多个 method() 和 Intercept() 方法进行拦截和修改，Byte Buddy 会按照栈的顺序来进行拦截。
     * <p>
     * 这里 method() 方法出现了三次，且每次出现后面都跟着的 intercept() 方法使用的 Implementation 参数都不同。
     * Byte Buddy 会按照栈的方式 将后定义 method() 方法放在栈顶，先定义的方法放在栈底。
     *
     * 在匹配方法的时候，按照下图执行出栈流程逐一匹配：《ByteBuddy匹配方法过程.png》
     */
    @Test
    public void test_3() throws InstantiationException, IllegalAccessException {
        Foo dynamicFoo = new ByteBuddy()
                .subclass(Foo.class)

                .method(ElementMatchers.isDeclaredBy(Foo.class)) // 匹配Foo中所有的方法
                .intercept(FixedValue.value("One!"))

                .method(ElementMatchers.named("foo")) // 匹配名为foo的方法
                .intercept(FixedValue.value("Two!"))

                .method(ElementMatchers.named("foo").and(ElementMatchers.takesArguments(1))) // 匹配名为foo且只有一个 参数的方法
                .intercept(FixedValue.value("Three!"))

                .make() // 生成
                .load(getClass().getClassLoader(), INJECTION) // 加载
                .getLoaded()
                .newInstance();

        System.out.println(dynamicFoo.bar());
        System.out.println(dynamicFoo.foo());
        System.out.println(dynamicFoo.foo(null));
    }

    /**
     * 通过 MethodDelegation 将拦截到的目标方法委托为另一个类去处理。
     * <p>
     * Interceptor01 中有三个方法，最终会委托给哪个方法呢？答案是 intercept(String name)  方法，委托并不是根据名称来的，
     * 而是和 Java 编译器在选重载时用的参数绑定类似。如果我们将 Intercept(String) 这个重载去掉，则 Byte Buddy 会选择
     * intercept(Object) 方法。可以尝试执行一下该示例，得到的输出分别是 String 和 Object。
     */
    @Test
    public void test_4() throws InstantiationException, IllegalAccessException {
        String helloWorld = new ByteBuddy()
                .subclass(DB.class)

                .method(ElementMatchers.named("hello")) // 拦截 DB.hello() 方法
                .intercept(MethodDelegation.to(Interceptor01.class)) // 委托到Interceptor的静态方法

                .make()
                .load(ClassLoader.getSystemClassLoader(), INJECTION)
                .getLoaded()
                .newInstance()
                .hello("World"); // 调用 hello() 方法

        System.out.println(helloWorld); // 拦截器返回的值
    }

    /**
     * 有一个地方需要注意，这里定义的 Interceptor02.intercept() 方法不是静态方法，而是一个实例方法。
     * 前面要委托到 Interceptor01 的静态方法，在 MethodDelegation.to() 方法中传递 Interceptor01.class。
     * 这里要委托到 Interceptor02 的实例方法，在 MethodDelegation.to() 方法中传递 Interceptor02 实例。
     */
    @Test
    public void test_5() throws InstantiationException, IllegalAccessException {
        String helloWorld = new ByteBuddy()
                .subclass(DB.class)

                .method(named("hello"))
                .intercept(MethodDelegation.to(new Interceptor02())) // 委托到 Interceptor02 的实例方法

                .make()
                .load(ClassLoader.getSystemClassLoader(), INJECTION)
                .getLoaded()
                .newInstance()
                .hello("World");

        System.out.println(helloWorld + "--");

        // 输出：
        // com.xxx.DB$ByteBuddy$8AV3B7GI@2d127a61
        // com.xxx.DB$ByteBuddy$8AV3B7GI@2d127a61
        // DB:World
        // null
    }

    /**
     * 前面示例中，使用 @SuperCall 注解注入的 Callable 参数来调用目标方法时，是无法动态修改参数的，
     * 如果想要动态修改参数，则需要用到 @Morph 注解以及一些绑定操作。
     * <p>
     * 这里的 Interceptor 会使用 @Morph 注解注入一个 OverrideCallable 对象作为参数，然后通过该
     * OverrideCallable 对象调用目标方法.
     */
    @Test
    public void test_6() throws InstantiationException, IllegalAccessException {
        String helloWorld = new ByteBuddy()
                .subclass(DB.class)
                .method(named("hello"))
                .intercept(MethodDelegation
                        .withDefaultConfiguration()
                        // 要用@Morph注解之前，需要通过 Morph.Binder 告诉 Byte Buddy 要注入的参数是什么类型
                        .withBinders(Morph.Binder.install(OverrideCallable.class))
                        .to(new Interceptor03()))
                .make()
                .load(Main.class.getClassLoader(), INJECTION)
                .getLoaded()
                .newInstance()
                .hello("World");

        System.out.println(helloWorld);
    }

    /**
     * 除了拦截 static 方法和实例方法，Byte Buddy 还可以拦截构造方法。
     * <p>
     * SuperMethodCall 会在新生成的方法中先调用目标方法，如果未找到目标方法则抛出异常，如果目标方法是构造方法，则根据方法签名匹配。
     * <p>
     * 不再使用 method() 方法拦截，而是使用 constructor() 方法拦截构造方法，并且使用 SuperMethodCall 调用构造方法并委托给 Interceptor 实例。
     */
    @Test
    public void test_7() throws Exception {
        Constructor<? extends DB> constructor = new ByteBuddy()
                .subclass(DB.class)
                .constructor(ElementMatchers.any()) // 通过constructor()方法拦截所有构造方法
                // 拦截的操作：首先调用目标对象的构造方法，根据前面自动匹配，这里直接匹配到参数为String.class的构造方法
                .intercept(SuperMethodCall.INSTANCE.andThen(
                        // 执行完原始构造方法，再开始执行interceptor的代码
                        MethodDelegation.withDefaultConfiguration().to(new Interceptor04())
                ))
                .make()
                .load(Main.class.getClassLoader(), INJECTION)
                .getLoaded()
                .getConstructor(String.class);
        // 下面通过反射创建生成类型的对象
        constructor.newInstance("MySQL");

        // 输出：
        // DB:MYSQL
        // after constructor!
    }

    /**
     * defineMethod() 方法：新增方法。
     * defineField() 方法：新增字段。
     * Implement() 方法：实现一个接口。
     */
    @Test
    public void test_8() throws Exception {
        Class<? extends Foo> loaded = new ByteBuddy()
                .subclass(Foo.class)

                .defineMethod("moon", String.class, Modifier.PUBLIC) // 新增一个方法，定义方法的名称、方法的返回值、public修饰
                .withParameter(String.class, "s") // 新增方法的参数
                .intercept(FixedValue.value("Zero!")) // 方法的具体实现，返回固定值

                .defineField("name", String.class, Modifier.PUBLIC) // 新增一个字段，该字段名称成为"name"，类型是 String，且public修饰

                .implement(DemoInterface.class) // 实现DemoInterface接口
                .intercept(FieldAccessor.ofField("name")) // 实现DemoInterface接口的方式是读写name字段

                .make()
                .load(Main.class.getClassLoader(), ClassLoadingStrategy.Default.INJECTION)
                .getLoaded(); // 获取加载后的Class

        Foo dynamicFoo = loaded.newInstance(); // 反射

        // 要调用新定义的moon()方法，只能通过反射方式
        Method m = loaded.getDeclaredMethod("moon", String.class);
        System.out.println(m.invoke(dynamicFoo, ""));

        Field field = loaded.getField("name"); // 通过反射方式读写新增的name字段
        field.set(dynamicFoo, "Zero-Name");
        System.out.println(field.get(dynamicFoo));

        // 通过反射调用DemoInterface接口中定义的get()和set()方法，读取name字段的值
        Method setNameMethod = loaded.getDeclaredMethod("set", String.class);
        setNameMethod.invoke(dynamicFoo, "Zero-Name2");
        Method getNameMethod = loaded.getDeclaredMethod("get");
        System.out.println(getNameMethod.invoke(dynamicFoo));

        // 输出如下：
        // Zero!
        // Zero-Name
        // Zero-Name2
    }

}